﻿using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Xml.Serialization;
using Bambit.XmlRpc.Dispatcher;
using log4net;

namespace Bambit.XmlRpc.Server
{
	/// <summary>
	/// This is a really basic implementation of an RpcServer.  Under most circumstances, it's usually
	/// easier to simply add a web page to handle the requeast, and dispatch from there.  However, if IIS
	/// is not availbale or you want a stand alone server, this is a basic template.
	/// </summary>
	public class RpcServer : IDisposable
	{
		#region Constants 
		
		private const int BUFFER_SIZE = 4096;
		
		#endregion Constants

		#region Class Data

		// ReSharper disable InconsistentNaming
		/// <summary>
		/// The logger
		/// </summary>
		private static readonly ILog _Logger = LogManager.GetLogger(typeof(RpcServer));
		// ReSharper restore InconsistentNaming

		/// <summary>
		/// The main body thread
		/// </summary>
		private Thread _MainThread;

		#endregion Class Data

		#region Properties

		/// <summary>
		/// Gets or sets a value indicating whether this instance is disposed.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if this instance is disposed; otherwise, <c>false</c>.
		/// </value>
		protected bool IsDisposed { get; set;}

		/// <summary>
		/// Gets or sets the dispatcher.
		/// </summary>
		/// <value>The dispatcher.</value>
		protected RpcDispatcher Dispatcher { get; private set; }

		/// <summary>
		/// The listener object
		/// </summary>
		///  NOTE: I had thought about using HttpListener for this, but, to my knowledge, there's no way to configure -- LPMIII 2009.07.17
		/// the port/IP address to bind that listener on, so I haven't looked into that solution very much
		protected TcpListener Listener { get; private set; }

		/// <summary>
		/// Gets or sets a value indicating when to stop the server.
		/// </summary>
		/// <value><c>true</c> if the server should be stopped; otherwise, <c>false</c>.</value>
		protected bool StopServer { get; set; }

		public bool IsRunning { get { return _MainThread != null; }
		}

		#endregion Properties

		#region Ctors

		/// <summary>
		/// Initializes a new instance of the <see cref="RpcServer"/> class.
		/// </summary>
		/// <param name="address">The address to bind the server to.</param>
		/// <param name="port">The port to listen on.</param>
		public RpcServer(IPAddress address, int port)
		{
			Listener=new TcpListener(address, port);
			Dispatcher=new RpcDispatcher();
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="RpcServer"/> class, listening
		/// on any available ip address
		/// </summary>
		/// <param name="port">The port to listen on.</param>
		public RpcServer(int port) : this(IPAddress.Any, port)
		{

		}

		#endregion Ctors

		#region Methods

		/// <summary>
		/// Adds a new <see cref="DispatchMethod"/>
		/// </summary>
		/// <param name="name">The name of the method call.</param>
		/// <param name="method">The method.</param>
		/// <remarks>Add a method that can be accessed</remarks>
		public void AddDispatchMethod(string name, DispatchMethod method)
		{
			if (_MainThread != null)
				throw new InvalidOperationException("Cannot modify dispatch methods when main thread is active");

			Dispatcher.Add(name, method);
		}

		/// <summary>
		/// Starts the server.
		/// </summary>
		public void Start()
		{
			_Logger.InfoFormat("Attempting to start server");
			if(_MainThread!=null)
				throw new InvalidOperationException("Server already running, cannot start");
			_MainThread=new Thread(Listen);
			StopServer = false;
			_MainThread.Start();
			_Logger.InfoFormat("Server started");
		}

		public void Stop()
		{
			_Logger.Info("Stopping server");
			StopServer = true;
			_MainThread.Join();
			_Logger.Info("Server stopped");
			_MainThread = null;
		}

		/// <summary>
		/// Listens for incoming connection, the main loop
		/// </summary>
		private void Listen()
		{
			Listener.Start();
			while (!StopServer)
			{
				if(Listener.Pending())
				{
					TcpClient client = Listener.AcceptTcpClient();
					Thread clientThread=new Thread(HandleClient);
					clientThread.Start(client);
				}
				else
				{
					Thread.Sleep(250);
				}
			}
			Listener.Stop();
		}
		private static string ReadStream(Stream clientStream)
		{
			byte[] buffer = new byte[BUFFER_SIZE];

			StringBuilder request = new StringBuilder();
			int numRead;
			do
			{
				try
				{
					numRead = clientStream.Read(buffer, 0, BUFFER_SIZE);
				}
				catch (Exception exc)
				{
					_Logger.Error("An error occured while tring to read from request stream", exc);
					throw;
					
				}
				if (numRead > 0)
					request.Append(Encoding.ASCII.GetString(buffer));
			} while (numRead == BUFFER_SIZE);
			return request.ToString();
		}

		/// <summary>
		/// Handles client connections
		/// </summary>
		/// <param name="clientObject">The client connection object.</param>
		private void HandleClient(object clientObject)
		{
			TcpClient client = clientObject as TcpClient;
			if(client==null)
			{
				_Logger.Error(string.Format("The supplied client is null (original client object ({0}))", clientObject));
				return;
			}
			NetworkStream clientStream = client.GetStream();
			string request;
			try
			{
				request = ReadStream(clientStream);
			}
			catch (Exception exc)
			{
				SendResponse(client,
				             new MethodResponse
				             	{
				             		Fault =
				             			Fault.CreateFault(-1,
				             			                  string.Format("An error occured while trying to read the request:\n:{0}",
				             			                                exc.Message))
				             	});
				return;
			}
			TextReader reader = new StringReader(request);
#if DEBUG
			///  NOTE: The headers are thrown away currently, so they are only here on debug builds -- LPMIII 2009.07.17
			StringBuilder headers=new StringBuilder();
#endif
			string line;
			bool expect100 = false;
			do
			{
				line = reader.ReadLine();
				headers.AppendLine(line);
				if (String.Compare("Expect: 100-Continue", line, true)==0)
					expect100 = true;
			} while (!String.IsNullOrEmpty(line));

			//	Check for good old expect 100
			if(expect100)
			{
				const string expectContinue = "HTTP/1.1 100 Continue\r\n\r\n";
				byte[] expectBuffer = Encoding.ASCII.GetBytes(expectContinue);
				clientStream.Write(expectBuffer,0,expectBuffer.Length);
				try
				{
					request = ReadStream(clientStream);
				}
				catch (Exception exc)
				{
					SendResponse(client,
								 new MethodResponse
								 {
									 Fault =
										 Fault.CreateFault(-1,
														   string.Format("An error occured while trying to read the request:\n:{0}",
																		 exc.Message))
								 });
					return;
				}
				reader = new StringReader(request);
			}

			XmlSerializer xmlSerializer=new XmlSerializer(typeof(MethodCall));
			MethodCall methodCall;
			try
			{
				methodCall = (MethodCall) xmlSerializer.Deserialize(reader);
			}
			catch (Exception exc)
			{
				_Logger.Error("An error was encountered while deserializing the method call", exc);
				SendResponse(client, new MethodResponse
				                     	{
				                     		Fault =
				                     			Fault.CreateFault(-2, string.Format("An error occured while reading the method call:\n{0}",
				                     			                                    exc.Message))
				                     	});
				return;
			}

			MethodResponse methodResponse;
			try
			{
				methodResponse = Dispatcher.Dispatch(methodCall);
			}
			catch (Exception exc)
			{
				_Logger.Error(string.Format("An error was encountered while trying to dispatch the method \"{0}\"",methodCall.MethodName),exc);
				SendResponse(client, new MethodResponse
				                     	{
				                     		Fault = Fault.CreateFault(-3, "An error occured while attempting to execute the requested method")
				                     	});
				return;
			}
			SendResponse(client, methodResponse);
		}

		private static void SendResponse(TcpClient client, MethodResponse response)
		{
			StringWriter payload=new StringWriter();
			XmlSerializer serializer=new XmlSerializer(typeof(MethodResponse));
			serializer.Serialize(payload,response);
			string payloadString = payload.ToString();
			StringWriter stringWriter=new StringWriter();
			stringWriter.Write(
				@"HTTP/1.1 200 OK
Connection: close
Content-Length: {0}
Content-Type: text/xml
Date: {1}
Server: Bambit.XmlRpcServer/1.0

{2}
",
				payloadString.Length, DateTime.Now.ToString("ddd, dd mmm yyyy hh:mm:ss Z"), payloadString);
			byte[] buffer = Encoding.ASCII.GetBytes(stringWriter.ToString());
			client.GetStream().Write(buffer, 0, buffer.Length);
			client.Close();
		}

		#endregion Methods

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Releases unmanaged and - optionally - managed resources
		/// </summary>
		/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
		public void Dispose(bool disposing)
		{
			if(!IsDisposed)
			{
				if (disposing)
				{
					if(IsRunning)
					{
						Stop();
					}
				}
				IsDisposed = true;
			}
		}

	}
}
